/*
    Copyright 2009 Semantic Discovery, Inc. (www.semanticdiscovery.com)

    This file is part of the Semantic Discovery Toolkit.

    The Semantic Discovery Toolkit is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    The Semantic Discovery Toolkit is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with The Semantic Discovery Toolkit.  If not, see <http://www.gnu.org/licenses/>.
*/
//import java.io.Serializable;
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SemanticDiscovery.Util.String;

[assembly: InternalsVisibleTo("SemanticDiscovery.UtilUnitTests")]

namespace SemanticDiscovery.Util.Range
{
  // todo: implement the Serializable interfaces

  /// <summary>
  /// Abstract base class for numeric ranges.
  /// </summary>
  /// <author>Spence Koehler</author>
  public abstract class AbstractNumericRange : NumericRange
  {
    protected abstract SimpleRange buildRange(string number);

    protected abstract SimpleRange buildRange(string left, 
                                              bool leftInclusive, 
                                              string right, 
                                              bool rightInclusive);

    protected abstract SimpleRange buildToleranceRange(string basee, 
                                                       string tolerance, 
                                                       bool inclusiveStart,
                                                       bool inclusiveEnd);

    private List<SimpleRange> ranges;
    private string str = null;

    /// Get the number of integers included in this range.
    /// @return null if the range is infinite; -1 if the number of integers 
    /// exceeds the maximum integer value, otherwise the size.
    public int? Count {
      get {
        int? size = null;

        foreach(SimpleRange range in ranges) {
          int? curSize = range.Count;
          if(!curSize.HasValue) {
            break;
          } else if(curSize.Value == -1) {
            size = -1;
            break;
          }

          if(size.HasValue)
            size += curSize.Value;
          else
            size = curSize.Value;
        }

        if(size.HasValue && size.Value == 0)
          return null;
        else
          return size;
      }
    }

    /// Get the (sorted) list of simple ranges.
    protected internal IList<SimpleRange> GetSimpleRanges() {
      ranges.Sort();
      return ranges;
    }

    protected AbstractNumericRange() {
      this.ranges = new List<SimpleRange>();
      this.str = null;
    }

    /// Determine whether the integer is in this numeric range.
    public bool Includes(int value) {
      bool result = false;

      foreach(SimpleRange range in ranges) {
        if(range.Includes(value)) {
          result = true;
          break;
        }
      }

      return result;
    }

    /// Determine whether the long is in this numeric range.
    public bool Includes(long value) {
      bool result = false;

      foreach(SimpleRange range in ranges) {
        if(range.Includes(value)) {
          result = true;
          break;
        }
      }

      return result;
    }

    /// Determine whether the double is in this numeric range.
    public bool Includes(double value) {
      bool result = false;

      foreach(SimpleRange range in ranges) {
        if(range.Includes(value)) {
          result = true;
          break;
        }
      }

      return result;
    }

    /// Determine whether the value is in this numeric range.
    public bool Includes(string value) {
      bool result = false;

      foreach(SimpleRange range in ranges) {
        if(range.Includes(value)) {
          result = true;
          break;
        }
      }

      return result;
    }

    /// Incorporate the value into this range.
    /// 
    /// See AbstractNumericRange.ParseValue's javadoc for the format of value.
    public void Include(string value) {
      ParseValue(value);
    }

    /// Shift all range end points by the given value.
    public void Shift(double value) {
      foreach(SimpleRange range in ranges) {
        range.Shift(value);
      }
    }

    /// Get this range fully represented as a string.
    public string AsString() {
      if(str == null) {
        StringBuilder result = new StringBuilder();
        foreach(SimpleRange range in GetSimpleRanges()) {  // so they're sorted
          if(result.Length > 0)
            result.Append(",");
          result.Append(range.AsString());
        }
        str = result.ToString();
      }
      return str;
    }

    public override string ToString() {
      return AsString();
    }

    /// Parse a collection of values. See ParseValue.
    protected void ParseValues(IEnumerable<string> values) {
      foreach(string value in values) {
        ParseValue(value);
      }
    }

    /// Parse a collection of values. See ParseValue.
    protected void ParseValues(string[] values) {
      foreach(string value in values) {
        ParseValue(value);
      }
    }

    /// Parse a value of the form: (for example)
    /// <p>
    /// "(a-b],c^d,[e-f)"
    /// <p>
    /// Where a, b, c, d, e, and f are integers or reals (depending on attributeType) and
    /// represent the range of values from a (exclusive) to b (inclusive), c plus or minus d
    /// (exclusive), and the values from e (inclusive) to f (exclusive).
    /// <p>
    /// There can be any number of terms separated by commas.
    /// <p>
    /// Unbounded ranges are supported such that: (where "a" is non-negative)
    /// <p>
    /// "a-" means all numbers from a to Integer.MAX_VALUE or Double.POSITIVE_INFINITY.
    /// <p>
    /// "-a-" means all numbers from negative a to Integer.MAX_VALUE or Double.POSITIVE_INFINITY.
    /// <p>
    /// "-(a)" means all numbers from Integer.MIN_VALUE or Double.NEGATIVE_INFINITY to (positive) a.
    /// <p>
    /// "-(-a)" or "--a" means all numbers from Integer.MIN_VALUE or Double.NEGATIVE_INFINITY to negative a.
    /// <p>
    /// "-" means all numbers from Integer.MIN_VALUE or Double.NEGATIVE_INFINITY to Integer.MAX_VALUE or Double.POSITIVE_INFINITY.
    /// <p>
    /// Note that:
    /// <p>
    /// "-a" means just the (negative) number "-a" is in the range.
    /// <p>
    /// An unbounded low or high always results in includesLow or includesHigh being true
    /// regardless of other annotations in the term.
    /// <p>
    /// A single value sets the low and high to that number and includesLow and includesHigh
    /// will be set to true regardless of other annotations in the term.
    protected void ParseValue(string value) {
      string[] pieces = value.RemoveWhitespace().Split(new char[] { ',' });

      foreach(string piece in pieces) {
        parsePiece(piece);
      }
    }

    /// Parse a piece of the form:
    /// <p>
    /// optional '(' or '['  (default is '[')
    /// optional number
    /// optional '-' or '^'
    /// optional number
    /// optional ')' or ']'  (default is ']')
    private void parsePiece(string piece) {
      int len = piece.Length;

      if(len == 0)
        return;

      // check for inclusive end
      char lastc = piece[len - 1];
      bool inclusiveEnd = (lastc != ')');  // default to inclusive if not explicitly exclusive
      if(lastc == ')' || lastc == ']')
        --len;

      // check for inclusive start
      int index = 0;
      char firstc = piece[index];
      bool inclusiveStart = (firstc != '(');  // default to inclusive if not explicitly exclusive
      if(firstc == '(' || firstc == '[')
        ++index;

      int markerIndex = -1;
      char marker = (char)0;

      if(firstc == '-') {
        marker = '-';

        if(len == 1) {
          // range is negative to positive infinity!
          markerIndex = 0;
        } else {  // len > 1
          // check for "-(" which indicates a range from negative infinity to a number.
          char secondc = piece[1];
          if(secondc == '(' || secondc == '[') {
            markerIndex = 1;
            ++index;
          } else if(secondc == '-') {
            // have "--a", which is negative infinity to negative a.
            markerIndex = 0;
            // leave index at 0.
          } else {
            markerIndex = findMarker(piece, 1);
            marker = (markerIndex > index) ? piece[markerIndex] : (char)0;
          }
        }
      } else {
        // check for range ('-') or tolerance ('^') marker
        markerIndex = findMarker(piece, index);
        if(markerIndex > index)
          marker = piece[markerIndex];
      }

      SimpleRange newRange = null;

      if(markerIndex < 0) {
        // no marker, just a number.
        string number = piece.Slice(index, len);
        newRange = buildRange(number);
      } else {
        string preMarkerString = piece.Slice(index, markerIndex);
        string postMarkerString = piece.Slice(markerIndex + 1, len);

        if(marker == '-') {
          newRange = buildRange(preMarkerString, inclusiveStart, postMarkerString, inclusiveEnd);
        } else if(marker == '^') {
          newRange = buildToleranceRange(preMarkerString, postMarkerString, inclusiveStart, inclusiveEnd);
        }
      }

      if(newRange != null) {
        incorporateRange(newRange);
      }
    }

    /// Find the index of a marker ('-' or '^') after index.
    private int findMarker(string piece, int index) {
      int result = -1;

      ++index;  // look AFTER the index

      int dashPos = piece.IndexOf('-', index);
      int caretPos = piece.IndexOf('^', index);

      if(dashPos < 0) {
        result = caretPos;
      } else if(caretPos < 0) {
        result = dashPos;
      } else {  // have both so only the caret could be meaningful
        result = caretPos;
      }

      return result;
    }

    protected void AddRange(SimpleRange newRange, bool combineIfContiguous) {
      if(combineIfContiguous) {
        incorporateRange(newRange);
      } else {
        addNonDuplicate(newRange);
      }
    }

    private void incorporateRange(SimpleRange newRange) {
      SimpleRange combined = null;
      foreach(SimpleRange range in ranges) {
        if(range.GetUnionIfContinuous(newRange)) {
          combined = range;
          break;
        }
      }

      if(combined == null) {
        ranges.Add(newRange);
      } else {
        // combining ranges may have brought two ranges together now. try to collapse.
        collapse(combined);
      }
    }

    private void addNonDuplicate(SimpleRange newRange) {
      bool isDup = false;
      foreach(SimpleRange range in ranges) {
        if(range.CompareTo(newRange) == 0) {
          isDup = true;
        }
      }
      if(!isDup) {
        ranges.Add(newRange);
      }
    }

    private void collapse(SimpleRange existingRange) {
      ranges.Remove(existingRange);
      incorporateRange(existingRange);
    }

    protected internal interface SimpleRange : NumericRange, IComparable<SimpleRange> {
      // todo: implement the Serializable interfaces

      /// Test whether this range includes its low value.
      bool IncludesLow { get; }
      /// Test whether this range includes its high value.
      bool IncludesHigh { get; }

      /// This range's low value (as a string).
      string Low { get; }
      /// The low value as an int.
      int LowAsInt { get; }
      int LowAsIntRounded { get; }
      /// The low value as an long.
      long LowAsLong { get; }
      long LowAsLongRounded { get; }
      /// The low value as a double.
      double LowAsDouble { get; }

      /// This range's high value (as a string).
      string High { get; }
      /// The high value as an int.
      int HighAsInt { get; }
      int HighAsIntRounded { get; }
      /// The high value as an long.
      long HighAsLong { get; }
      long HighAsLongRounded { get; }
      /// The high value as a double.
      double HighAsDouble { get; }

      /// Incorporate the other range's values into this range if this range
      /// combined with the other range is continuous.
      bool GetUnionIfContinuous(SimpleRange other);

      /// Shift this range's bounds by the given value.
      void Shift(double value);
    }

  }
}
